package gecp.edge;

import gecp.edge.image.Image;
import gecp.math.Angle;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;

/**
 * Edge detector class
 *
 * @author Cosmin
 */
public class Detector {

    /**
     * If this flag is set to true then the application will use cache file for
     * computed forces
     */
    public static boolean useCache = false;
    /**
     * If this flag is set tot true then the application will generate PNG file
     * with the output for debug
     */
    public static boolean usePNG = false;
    /**
     * The file path for the file that we processing
     */
    public String filename;
    /**
     * The step for computations
     */
    public int step;
    /**
     * The thresholder for computations
     */
    public double thresholder;
    /**
     * The edge flag matrix
     */
    public boolean[][] isEdge;
    /**
     * The line flag matrix
     */
    public boolean[][] isLine;
    /**
     * The list of all lines that are edges
     */
    public List<Line> lines;
    /**
     * The image for computing edges by forces
     */
    protected Image _image;

    /**
     * The constructor of the detector
     *
     * @param filemame The file path for the file that we processing
     */
    public Detector(String filemame) {
        this.filename = filemame;
    }

    /**
     * Computing the edges
     *
     * @param step The step for computations
     * @param thresholder The thresholder for computations
     */
    public void compute(int step, double thresholder) {
        this.step = step;
        this.thresholder = thresholder;
        this._image = new Image(this.filename);
        this._image.compute(this.step, this.thresholder);
        this._computeEdges();
        this._generateEdgePNG(this.filename + "-2.sigle.edge." + this.step + ".png");
        this._computeLines();
        this._generateLineEdgePNG(this.filename + "-2.line.edge." + this.step + ".png");
    }

    /**
     * Compute single pixel edges from multi pixel edges computed with forces
     * algorithm
     */
    protected void _computeEdges() {
        this.isEdge = new boolean[this._image.width][this._image.height];
        for (int i = 0; i < this._image.width; i++) {
            for (int j = 0; j < this._image.height; j++) {
                this.isEdge[i][j] = false;
            }
        }
        for (int i = 0; i < this._image.width; i++) {
            for (int j = 0; j < this._image.height; j++) {
                if (!this._hasEdgeNeighbor(i, j)) {
                    this._computePixel(i, j, i, j);
                }
            }
        }
    }

    /**
     * Compute the single edge for an pixel. If for the current pixel we can
     * compute a neighbor that is on the current edge the we go to it.
     *
     * @param x The coordinate on x axis
     * @param y The coordinate on y axis
     * @param from_x The coordinate on x axis for the previous pixel
     * @param from_y The coordinate on y axis for the previous pixel
     */
    protected void _computePixel(int x, int y, int from_x, int from_y) {
        /**
         * First we test if we can build a line (the next pixel on the same
         * direction as the previous is on edge)
         */
        int diff_x = from_x - x;
        int diff_y = from_y - y;
        int next_x = x;
        int next_y = y;
        if (diff_x != 0 || diff_y != 0) {
            diff_x *= -1;
            diff_y *= -1;
            next_x += diff_x;
            next_y += diff_y;
            if (!this._hasEdgeNeighbor(next_x, next_y)) {
                /**
                 * We have a neighbor that is on the edge and on the direction
                 * as the previous pixel
                 */
                this.isEdge[x][y] = true;
                this._computePixel(next_x, next_y, x, y);
                return;
            }
        }
        /**
         * If we can't build a line we search for a neighbor to be on the edge
         */
        int index = -1;
        double indexAngle = 360;
        for (int i = 0; i < 8; i++) {
            next_x = this._getNeighborX(x, i);
            next_y = this._getNeighborY(y, i);
            if (!this._hasEdgeNeighbor(next_x, next_y)) {
                double angleDiff = Angle.absAngleDifference(this._image.forceAngle[x][y], this._image.forceAngle[next_x][next_y]);
                if (angleDiff < indexAngle) {
                    index = i;
                    indexAngle = angleDiff;
                }
            }
        }
        this.isEdge[x][y] = true;
        if (index > -1) {
            /**
             * We have a neighbor that is on the current edge
             */
            next_x = this._getNeighborX(x, index);
            next_y = this._getNeighborY(y, index);
            this._computePixel(next_x, next_y, x, y);
        }
    }

    /**
     * Return true if the current pixel has a neighbor in an edge or is not on
     * edge computed by the forces
     *
     * @param x The coordinate on x axis
     * @param y The coordinate on y axis
     *
     * @return True if the pixel must be skipped
     */
    protected boolean _hasEdgeNeighbor(int x, int y) {
        if (this._image.isEdge[x][y]) {
            for (int i = 0; i < 8; i++) {
                int mx = this._getNeighborX(x, i);
                int my = this._getNeighborY(y, i);
                if (mx >= 0 && mx < this._image.width && my >= 0 && my < this._image.height) {
                    if (this.isEdge[mx][my]) {
                        return true;
                    }
                } else {
                    return true;
                }
            }
        } else {
            return true;
        }
        return false;
    }

    /**
     * Get neighbor coordinate on x axis for an index
     *
     * @param x The pixel coordinate on x axis
     * @param index The index
     *
     * @return Neighbor coordinate on x axis
     */
    protected int _getNeighborX(int x, int index) {
        switch (index) {
            case 0:
                return x - 1;
            case 1:
                return x - 1;
            case 2:
                return x - 1;
            case 3:
                return x;
            case 4:
                return x + 1;
            case 5:
                return x + 1;
            case 6:
                return x + 1;
            case 7:
                return x;
        }
        return x;
    }

    /**
     * Get neighbor coordinate on y axis for an index
     *
     * @param y The pixel coordinate on y axis
     * @param index The index
     *
     * @return Neighbor coordinate on y axis
     */
    protected int _getNeighborY(int y, int index) {
        switch (index) {
            case 0:
                return y - 1;
            case 1:
                return y;
            case 2:
                return y + 1;
            case 3:
                return y + 1;
            case 4:
                return y + 1;
            case 5:
                return y;
            case 6:
                return y - 1;
            case 7:
                return y - 1;
        }
        return y;
    }

    /**
     * Generate and save to HDD a PNG file with all the pixel on edges
     *
     * @param filename The file to save
     */
    protected void _generateEdgePNG(String filename) {
        if (!Detector.usePNG) {
            return;
        }
        System.out.println("Start render the single pixel edge png of " + this._image.width + "x" + this._image.height);
        BufferedImage bi = new BufferedImage(this._image.width, this._image.height, BufferedImage.TYPE_INT_ARGB);
        try {
            for (int i = 0; i < this._image.width; i++) {
                for (int j = 0; j < this._image.height; j++) {
                    if (this.isEdge[i][j]) {
                        int rgb = Color.RED.getRGB();
                        bi.setRGB(i, j, rgb);
                    }
                }
            }
            Detector.writePNGToHDD(filename, bi);
        } catch (Exception ex) {
            Logger.getLogger(Detector.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Generate and save to HDD a PNG file with all the lines who are edges
     *
     * @param filename The file to save
     */
    protected void _generateLineEdgePNG(String filename) {
        if (!Detector.usePNG) {
            return;
        }
        System.out.println("Start render the lines edge png of " + this._image.width + "x" + this._image.height);
        BufferedImage bi = new BufferedImage(this._image.width, this._image.height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D ig2 = bi.createGraphics();
        Random random = new Random();
        try {
            for (Line line : this.lines) {
                int rgb = random.nextInt();
                ig2.setColor(new Color(rgb));
                ig2.drawLine(line.from.x, line.from.y, line.to.x, line.to.y);
            }
            Detector.writePNGToHDD(filename, bi);
        } catch (Exception ex) {
            Logger.getLogger(Detector.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Computes all the lines from pixels on edge
     */
    protected void _computeLines() {
        this.lines = new ArrayList<Line>();
        this.isLine = new boolean[this._image.width][this._image.height];
        for (int i = 0; i < this._image.width; i++) {
            for (int j = 0; j < this._image.height; j++) {
                this.isLine[i][j] = false;
            }
        }
        for (int i = 0; i < this._image.width; i++) {
            for (int j = 0; j < this._image.height; j++) {
                if (this.isEdge[i][j] && !this.isLine[i][j]) {
                    this._computeLineForPixel(i, j);
                }
            }
        }
    }

    /**
     * Compute a line who starts from a pixel and add that line to the lines
     * list if is longer the 1 pixel (start != end)
     *
     * @param x The coordinate on x axis for current pixel
     * @param y The coordinate on y axis for current pixel
     */
    protected void _computeLineForPixel(int x, int y) {
        Location start = new Location(x, y);
        this.isLine[x][y] = true;
        Location end = this._computeEndForLine(x, y, x, y);
        if (start.x != end.x || start.y != end.y) {
            this.lines.add(new Line(start, end));
        } else {
            this.isLine[x][y] = false;
        }
    }

    /**
     * Compute the end of a line recursively
     *
     * @param x The coordinate on x axis for current pixel
     * @param y The coordinate on y axis for current pixel
     * @param from_x The coordinate on x axis for previous pixel
     * @param from_y The coordinate on y axis for previous pixel
     *
     * @return The location of the end of the line
     */
    protected Location _computeEndForLine(int x, int y, int from_x, int from_y) {
        int diff_x = from_x - x;
        int diff_y = from_y - y;
        int next_x = x;
        int next_y = y;
        if (diff_x != 0 || diff_y != 0) {
            this.isLine[x][y] = true;
            /**
             * If is not the start pixel, we have at least 2, we search for the
             * next pixel on the same direction as previous one
             */
            diff_x *= -1;
            diff_y *= -1;
            next_x += diff_x;
            next_y += diff_y;
            if (this.isEdge[next_x][next_y]) {
                return this._computeEndForLine(next_x, next_y, x, y);
            }
        } else {
            /**
             * If we are at the first pixel, we search for a neighbor who is on
             * edge
             */
            for (int i = 0; i < 8; i++) {
                next_x = this._getNeighborX(x, i);
                next_y = this._getNeighborY(y, i);
                if (this.isEdge[next_x][next_y]) {
                    return this._computeEndForLine(next_x, next_y, x, y);
                }
            }
        }
        return new Location(x, y);
    }

    /**
     * Write a PNG to HDD into png special folder
     *
     * @param filename The original filename of the PNG
     * @param bufferedImage The image to write to PNG
     *
     * @throws IOException
     */
    public static void writePNGToHDD(String filename, BufferedImage bufferedImage) throws IOException {
        if (!Detector.usePNG) {
            return;
        }
        File origFile = new File(filename);
        File writeFile = new File("png/" + origFile.getName());
        System.out.println("Writing PNG " + origFile.getName() + " to HDD into png/" + origFile.getName());
        ImageIO.write(bufferedImage, "PNG", writeFile);
    }
}
